Repository navigation
modules: fix sync hook short-circuit in require() in imported CJS - #62920
Conversation
|
Review requested:
|
|
cc @mcollina as #55808 (comment) referenced https://github.com/platformatic/vfs |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #62920 +/- ##
==========================================
- Coverage 91.49% 89.65% -1.84%
==========================================
Files 360 712 +352
Lines 151674 220633 +68959
Branches 23919 42293 +18374
==========================================
+ Hits 138772 197815 +59043
- Misses 12625 14662 +2037
- Partials 277 8156 +7879
🚀 New features to boost your workflow:
|
1ef6e6e to
53673f3
Compare
|
Why still some test fails is it a issue in code or it just cope(some infra issue) ? |
|
It's just regular flake. In the past ~8 years it would be miracle for a PR to pass the CI fully without a flake 😬 https://github.com/nodejs/reliability/issues |
Commit Queue failed- Loading data for nodejs/node/pull/62920 ✔ Done loading data for nodejs/node/pull/62920 ----------------------------------- PR info ------------------------------------ Title modules: fix sync hook short-circuit in require() in imported CJS (#62920) Author Joyee Cheung <joyeec9h3@gmail.com> (@joyeecheung) Branch joyeecheung:fix-hooks -> nodejs:main Labels module, esm, needs-ci, review wanted Commits 1 - module: fix sync hook short-circuit in require() in imported CJS Committers 1 - Joyee Cheung <joyeec9h3@gmail.com> PR-URL: https://github.com/nodejs/node/pull/62920 Fixes: https://github.com/nodejs/node/issues/63060 Reviewed-By: Paolo Insogna <paolo@cowtech.it> ------------------------------ Generated metadata ------------------------------ PR-URL: https://github.com/nodejs/node/pull/62920 Fixes: https://github.com/nodejs/node/issues/63060 Reviewed-By: Paolo Insogna <paolo@cowtech.it> -------------------------------------------------------------------------------- ℹ This PR was created on Thu, 23 Apr 2026 22:07:13 GMT ✔ Approvals: 1 ✔ - Paolo Insogna (@ShogunPanda) (TSC): https://github.com/nodejs/node/pull/62920#pullrequestreview-4211272383 ✔ Last GitHub CI successful ℹ Last Full PR CI on 2026-05-01T16:34:45Z: https://ci.nodejs.org/job/node-test-pull-request/73060/ - Querying data for job/node-test-pull-request/73060/ ✔ Build data downloaded ✔ Last Jenkins CI successful -------------------------------------------------------------------------------- ✔ No git cherry-pick in progress ✔ No git am in progress ✔ No git rebase in progress -------------------------------------------------------------------------------- - Bringing origin/main up to date... From https://github.com/nodejs/node * branch main -> FETCH_HEAD ✔ origin/main is now up-to-date - Downloading patch for 62920 From https://github.com/nodejs/node * branch refs/pull/62920/merge -> FETCH_HEAD ✔ Fetched commits as 9c0101860e77..53673f349d9a -------------------------------------------------------------------------------- Auto-merging lib/internal/modules/cjs/loader.js Auto-merging lib/internal/modules/esm/loader.js [main a31ccfd3be] module: fix sync hook short-circuit in require() in imported CJS Author: Joyee Cheung <joyeec9h3@gmail.com> Date: Sat Apr 18 22:10:36 2026 +0100 6 files changed, 265 insertions(+), 89 deletions(-) create mode 100644 test/module-hooks/test-module-hooks-load-import-cjs-custom-source.js ✔ Patches applied -------------------------------------------------------------------------------- --------------------------------- New Message ---------------------------------- module: fix sync hook short-circuit in require() in imported CJS
PR-URL: #62920
|
|
I think the issue related must be closed #63060 by now. |
|
Btw @joyeecheung , should i look into the flaky test cases and try resolving them so that after 8 year someone might get the miracle done ;) |
… invalidated vitest 4 → 5, pnpm 11 → 12, @oxc-node/core 0.1.0 → 0.1.2, oxfmt 0.64 → 0.68, oxlint 1.79 → 1.83, @rspack/core 2.1 → 2.2, rolldown 1.2.5 → 1.2.8, playwright 1.62 → 1.63, plus @types/node and fast-xml-parser. `@oxc-node/core` 0.1.2 does not remove the need for the patch: `register.mjs` is byte-identical from 0.1.0 through 0.1.2 and on upstream `main` — verified with `cmp`, both at blob 9658531 — and the 0.1.1/0.1.2 changelogs only cover transform, resolve and UNC-path fixes. Dropping the patch brought DEP0205 back to every `node --import @oxc-node/core/register`, which is every gate script here. The patch is rebuilt rather than re-keyed, because the old one was wrong in a way this package never exercised: it routed a CommonJS `require()` into oxc's resolver, which rejects a context without `importAttributes`, so any `.ts` entry in a `"type": "commonjs"` scope died with ``Missing field `importAttributes` ``. Nothing here is CommonJS, so it never showed. The replacement keeps `require()` on Node's own resolution, hands a file oxc reports as CommonJS back to the `pirates` hook so its source map stays accurate, and gates `registerHooks` on Node >= 26.2 — it exists from 22.15 but only became a complete replacement once nodejs/node#59011 (24.5.0, backported to 22.19.0) and nodejs/node#62920 (26.2.0) landed. Below that the deprecated path still runs, so nothing regresses. Measured across 34 release binaries from 22.14.0 to 26.8.2. Submitted upstream as oxc-project/oxc-node#764. The satellite's shared devDependencies move to a pnpm catalog. It had been left on vitest 4 while the root went to 5, which put two vitest majors in the tree; declaring the version once makes that drift structurally impossible, and `pnpm pack` rewrites `catalog:` to a real range so a consumer never sees it. `allowBuilds.esbuild` is now false. esbuild is still used — `scripts/treeshake- verify.ts` is one of its three bundlers — but its postinstall only speeds up the `esbuild` CLI, which nothing here invokes; the JS API spawns the platform binary directly. `buildSync` and all 111 treeshake scenarios pass without it. The two stream tests are reformatted by oxfmt 0.68, which breaks a trailing function expression's parameters differently. No behaviour change. Verified: 20397 node tests, 14454 browser tests, 592 satellite tests, `pnpm check`, and `pnpm verify:treeshake` 111/111.
Two requests the asynchronous `module.register()` never showed the hooks reach them
under `module.registerHooks()`, and both regressed:
- Its default load is synchronous and always reads the file, so a `commonjs` result
arrives with its bytes attached — including for a `.node` addon, which resolves as
`commonjs`, and for a latin-1 CommonJS file. `transform_output` then failed to decode
them ("Failed to convert Uint8Array to Vec<u8>"), so `import addon from './x.node'`
crashed where it printed an object before. Hand such a source back the way the
asynchronous default load did — no source, CommonJS machinery reads the file — instead
of propagating the decode error.
- `require()` was told apart by the `require` export condition, but `--conditions`
appends its values to *every* request: under `--conditions=require` an extensionless
`import` failed with ERR_MODULE_NOT_FOUND and a CommonJS-reported file with ESM syntax
with ERR_REQUIRE_CYCLE_MODULE, and `--conditions=import` is the mirror hazard.
Discriminate on `importAttributes` instead — the field the native hooks require anyway,
absent on every CommonJS context and present, if empty, on every ESM one.
The load hook now returns `{ source: null }` for a CommonJS result rather than calling
`nextLoad` a second time: same outcome, one file read and one transform less, and it is
the shape that never trips nodejs/node#62920, so that defect no longer bears on the
version floor — v26.2.0 is kept as a deliberate margin. CI gains a Node.js 26 job, since
nothing in the matrix exercised the synchronous path.
## Problem `packages/core/register.mjs` calls `module.register()`. Node.js runtime-deprecated that API in **v25.9.0** ([DEP0205](https://nodejs.org/api/deprecations.html#DEP0205)), so every `node --import @oxc-node/core/register` now prints: ``` (node:94305) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead. ``` This is not only cosmetic — **`pnpm test` already fails on Node.js 26** on `main`, because four tests assert an empty stderr and the warning lands there. CI runs Node 22 and 24 only, so it has stayed invisible: ``` # main, unmodified, Node v26.8.1 Test Files 2 failed | 7 passed | 1 skipped (10) Tests 4 failed | 70 passed | 7 skipped (81) FAIL __tests__/stdin-tty.spec.ts > CLI properly handles stdin piping FAIL __tests__/tsconfig-discovery.spec.ts > a broken project reference in an ancestor that does not own the file breaks nothing FAIL __tests__/tsconfig-discovery.spec.ts > tsconfig paths apply to JavaScript importers with allowJs unset FAIL __tests__/tsconfig-discovery.spec.ts > a project directory with a space and non-ASCII characters still resolves ``` All four are the same diff: `+ (node:1427) [DEP0205] DeprecationWarning: ...` ## Change Use the synchronous, in-thread `module.registerHooks()` where it is complete, and keep `module.register()` everywhere else. The ESM path is otherwise unchanged — `resolve` still hands everything to `createResolve` and `load` to `oxcLoad`, exactly as `esm.mjs` does. Three things make this more than a one-line swap. Each was measured against release binaries rather than reasoned about, and a naive replacement fails on all three. ### 1. `registerHooks()` shows `require()` to the hooks; `register()` never did The CommonJS resolve/load context carries **no `importAttributes`**, and both `createResolve` and `load` reject a context without it: ``` Error: Missing field `importAttributes` at resolve (.../register.mjs:11:12) at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1217:25) ``` So a request whose `conditions` contain `require` goes to `nextResolve`/`nextLoad`. Node.js resolves those correctly by itself, because the `pirates` hook has put the TypeScript extensions into `Module._extensions` — that is what lets its CommonJS resolver complete `require('./dep')` to `./dep.ts` and `require('./sub')` to `./sub/index.ts`. ### 2. A file that oxc-node settles on as CommonJS must go back to `nextLoad` Otherwise the `pirates` hook no longer produces the inline source map, and stack trace precision regresses. `cli.spec.ts` covers exactly this: | | reported location | |---|---| | `module.register()` | `stacktrace-cjs.cts:6:12` ✅ | | naive `registerHooks()` | `stacktrace-cjs.cts:5:10` ❌ | Calling `oxcLoad` **first** and only then deferring is what keeps #759 intact — a CommonJS-reported file that actually contains ESM syntax still runs as an ES module. ### 3. `registerHooks` needs a version floor, not a `typeof` check It exists from v22.15.0/v23.5.0, but two defects had to be fixed first. Bisected over every available minor: | Node.js | ESM importing a CJS package | `require()` in an imported CJS entry | |---|---|---| | 22.14.0 (no `registerHooks`) | — fallback | — fallback | | 22.15.0 – 22.18.0 | ❌ | ❌ | | 22.19.0 – 22.22.1 | ✅ | ❌ | | 23.5.0 – 23.11.1 (EOL) | ❌ | ❌ | | 24.0.0 – 24.4.1 | ❌ | ❌ | | 24.5.0 – 25.9.0 | ✅ | ❌ | | 26.0.0 – 26.1.0 | ✅ | ❌ | | **26.2.0+** | ✅ | ✅ | Those boundaries are not arbitrary — they match the Node.js fixes exactly: - **[nodejs/node#59011](nodejs/node#59011 *"module: fix conditions override in synchronous resolve hooks"* — v24.5.0 (`fe0195fdcc`), backported to v22.19.0 (`0eec5cc492`), never backported to the end-of-life 23.x line. Without it, `import { jsx } from 'react/jsx-runtime'` fails with *does not provide an export named 'jsx'*. - **[nodejs/node#62920](nodejs/node#62920 *"module: fix sync hook short-circuit in require() in imported CJS"* — v26.2.0 (`96f19a16d0`). Without it, a `.ts` entry in a CommonJS package cannot `require()` its own files. Hence the floor is **v26.2.0**. Below it the fallback runs and nothing changes for those runtimes; v26.0/v26.1 keep the warning, which is the honest outcome given the defect. ## Verification **34 release binaries**, v22.14.0 → v26.8.2 (every available 22.x/23.x/24.x/25.x/26.x minor), against three fixtures: a CommonJS `require()` tree (extensionless `.ts`, directory `index.ts`, explicit `.cts`), an ESM tree (extensionless, `.mts`, tsconfig `paths`, `.tsx` + `react/jsx-runtime`), and a `createRequire` tree. ``` versions tested: 34 failures: 0 still warning: 26.0.0 26.1.0 ``` Upstream suite, same machine, Node v26.8.1: | | result | |---|---| | `main` | `4 failed | 70 passed` | | this PR | **`75 passed`** | `vp fmt` and `vp lint` are clean. ## Test added `require()` completing an extensionless TypeScript specifier had no coverage. The new case in `cjs-esm-syntax.spec.ts` fails with `Cannot find module './dep'` if the `pirates` hook stops registering those extensions — verified by removing it. --------- Co-authored-by: LongYinan <lynweklm@gmail.com>
…script compilerFor required typescript through the hooks it registers, so a failed lookup from a .ts file asked for the compiler again before the cache held an entry, until the stack overflowed. A bare catch swallowed the RangeError on arm64, where the suite passed; amd64 Node 22 aborted in V8's regexp compiler instead. The entry is now settled before the require, and the catches rethrow anything but a resolution failure, so a recurrence fails on every host. Node before 24.18 and 26.2 gives CommonJS that an ES module imported a require() that skips module hooks (nodejs/node#62920). The alias test now runs through a CommonJS entry on every hook-capable Node, the ES module entry runs only where Node has the fix, and the guide says so. CI prints each host lane's Node: the ubuntu runners use their image's Node 22, not the distro's.
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guide: collect every difference that can need a change when moving from tsx into one section: `paths` outside `include`, the JSX default and lone `@jsxImportSource`, `.cts` files with module syntax, the Node.js range, watch mode, eval and the REPL, `node --import tsx`, and other loaders. RFC: - Known Limitations: the in-thread window without nodejs/node#62920, and the project-local bin not forwarding a SIGINT sent to it alone. - A CLI/loader contract change gets new entry file names, so an unaware `vp` falls back to the global loader. - Source maps for node_modules, and their startup cost. - Security: project code provides the loader, as for `vp` delegation. - The daily upgrade can stay red while oxc-node needs a newer oxc than Rolldown. - Phase 2 migrates tsx only, gated on the Node.js range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guide: collect every difference that can need a change when moving from tsx into one section: `paths` outside `include`, the JSX default and lone `@jsxImportSource`, `.cts` files with module syntax, the Node.js range, watch mode, eval and the REPL, `node --import tsx`, and other loaders. RFC: - Known Limitations: the in-thread window without nodejs/node#62920, and the project-local bin not forwarding a SIGINT sent to it alone. - A CLI/loader contract change gets new entry file names, so an unaware `vp` falls back to the global loader. - Source maps for node_modules, and their startup cost. - Security: project code provides the loader, as for `vp` delegation. - The daily upgrade can stay red while oxc-node needs a newer oxc than Rolldown. - Phase 2 migrates tsx only, gated on the Node.js range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node.js 22.22.3 and later use module.registerHooks(), decided by a separate branch of canRegisterSyncHooks, and 22.x never gets nodejs/node#62920, but no fixture ran that combination: the main fixture pins 22.18 (module.register()) and the others 24.12 and 26.5. Add a Node.js 22.23.2 fixture with the same steps as the 24.12 one, which on 22.x also lowers `using` to a runtime helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guide: collect every difference that can need a change when moving from tsx into one section: `paths` outside `include`, the JSX default and lone `@jsxImportSource`, `.cts` files with module syntax, the Node.js range, watch mode, eval and the REPL, `node --import tsx`, and other loaders. RFC: - Known Limitations: the in-thread window without nodejs/node#62920, and the project-local bin not forwarding a SIGINT sent to it alone. - A CLI/loader contract change gets new entry file names, so an unaware `vp` falls back to the global loader. - Source maps for node_modules, and their startup cost. - Security: project code provides the loader, as for `vp` delegation. - The daily upgrade can stay red while oxc-node needs a newer oxc than Rolldown. - Phase 2 migrates tsx only, gated on the Node.js range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node.js 22.22.3 and later use module.registerHooks(), decided by a separate branch of canRegisterSyncHooks, and 22.x never gets nodejs/node#62920, but no fixture ran that combination: the main fixture pins 22.18 (module.register()) and the others 24.12 and 26.5. Add a Node.js 22.23.2 fixture with the same steps as the 24.12 one, which on 22.x also lowers `using` to a runtime helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guide: collect every difference that can need a change when moving from tsx into one section: `paths` outside `include`, the JSX default and lone `@jsxImportSource`, `.cts` files with module syntax, the Node.js range, watch mode, eval and the REPL, `node --import tsx`, and other loaders. RFC: - Known Limitations: the in-thread window without nodejs/node#62920, and the project-local bin not forwarding a SIGINT sent to it alone. - A CLI/loader contract change gets new entry file names, so an unaware `vp` falls back to the global loader. - Source maps for node_modules, and their startup cost. - Security: project code provides the loader, as for `vp` delegation. - The daily upgrade can stay red while oxc-node needs a newer oxc than Rolldown. - Phase 2 migrates tsx only, gated on the Node.js range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node.js 22.22.3 and later use module.registerHooks(), decided by a separate branch of canRegisterSyncHooks, and 22.x never gets nodejs/node#62920, but no fixture ran that combination: the main fixture pins 22.18 (module.register()) and the others 24.12 and 26.5. Add a Node.js 22.23.2 fixture with the same steps as the 24.12 one, which on 22.x also lowers `using` to a runtime helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guide: collect every difference that can need a change when moving from tsx into one section: `paths` outside `include`, the JSX default and lone `@jsxImportSource`, `.cts` files with module syntax, the Node.js range, watch mode, eval and the REPL, `node --import tsx`, and other loaders. RFC: - Known Limitations: the in-thread window without nodejs/node#62920, and the project-local bin not forwarding a SIGINT sent to it alone. - A CLI/loader contract change gets new entry file names, so an unaware `vp` falls back to the global loader. - Source maps for node_modules, and their startup cost. - Security: project code provides the loader, as for `vp` delegation. - The daily upgrade can stay red while oxc-node needs a newer oxc than Rolldown. - Phase 2 migrates tsx only, gated on the Node.js range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node.js 22.22.3 and later use module.registerHooks(), decided by a separate branch of canRegisterSyncHooks, and 22.x never gets nodejs/node#62920, but no fixture ran that combination: the main fixture pins 22.18 (module.register()) and the others 24.12 and 26.5. Add a Node.js 22.23.2 fixture with the same steps as the 24.12 one, which on 22.x also lowers `using` to a runtime helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix shipped in 26.2.0 and was backported to 24.18.0, not to 22.x. Since the oxc-node#820 sync, the in-thread hooks return CommonJS source that another hook supplied, so record the releases where require() inside such a module can still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: #55808 (comment)
Fixes: #63060